開發 PawPal 時,我遇過不少會直接跳出紅色錯誤訊息的問題。
但這次不一樣。
當時 Dashboard 上的寵物名稱和照片都正常,只有年齡顯示成 -。
畫面沒有壞掉,Console 也沒有直接告訴我答案。
我不知道問題是在元件、API,還是日期格式,只能從眼前的結果一步一步往回查。
在還沒串接真實 API 時,前端使用的 mock data 本來就已經有可以直接顯示的年齡資料。
概念上就像:
const pet = {
name: '阿黑',
age: '2歲',
}
畫面只需要讀取:
pet.age
就能把年齡顯示出來。
但後來改成串接真實 API,資料來源變了。
後端並沒有直接幫前端算好 age,而是提供寵物的:
birthday
概念上就像:
const pet = {
name: '阿黑',
birthday: '2024-05-10',
}
當時的 PetCard 還在讀:
pet.age
所以串上真實資料後,這個值變成空的,最後畫面就顯示原本設定的 fallback:
-
這也是這次 Bug 真正有趣的地方。
API 其實有正常回傳資料。
寵物名稱和照片也都存在。
只有年齡的資料來源改變了,但前端顯示方式還沒有一起調整。
剛開始看到畫面時,我當然還不知道問題就在這裡。
因為畫面沒有提供足夠的線索,我當時先用 console.log() 把取得的寵物資料印出來。
console.log(pet)
這個動作看起來很簡單,但對當時的我來說很重要。
因為在沒有方向的情況下,我至少可以先確認:
API 到底有沒有回傳資料?
→ 回傳了哪些欄位?
→ 年齡相關資料叫什麼名字?
→ 欄位裡實際放了什麼內容?
把資料攤開來看之後,我才慢慢發現:
不是 API 完全沒有回傳資料,而是前端想讀取的欄位,和 API 實際提供的欄位不一樣。
原本畫面期待的是:
pet.age
但真實資料提供的是:
pet.birthday
到這裡,問題的範圍一下就小很多。
它不是整支 API 壞掉。
也不是整張寵物卡片失效。
真正需要處理的是:
既然後端給我的是生日,那前端要怎麼把生日轉成年齡?
從這次開始,我才真正理解,console.log() 不只是把資料印出來。
它也可以幫助我確認現在到底拿到了什麼,然後一步一步排除不相關的可能性。
確認 API 提供的是 birthday 後,下一步很自然:
那就用生日算年齡。
聽起來很簡單。
但真的開始寫時,我又遇到新的問題。
生日從 API 回來時是一段日期資料,我需要先確定:
生日有沒有值
→ 日期能不能正常解析
→ 目前日期和生日差多少
→ 最後要怎麼顯示成年齡
專案原本就已經有處理寵物生日顯示的共用函式 formatPetBirthday()。
它會處理像:
YYYY-MM-DD
以及 API 常見的 ISO 日期時間格式,再整理成畫面需要的生日顯示格式。
處理這次年齡 Bug 時,我又加入了:
formatPetAge(birthday)
讓寵物年齡可以直接由 birthday 計算。
最後畫面可以依照年齡顯示:
2歲3個月
8個月
未滿1個月
如果沒有生日、日期無法解析,或生日是在未來,則會回傳:
-
這裡我後來重新看 Repository 才發現一件事。
目前的 formatter 並不是完整的「日期合法性驗證器」。
像某些外觀看起來符合 YYYY-MM-DD、實際日期卻不存在的值,不一定都會被判定成錯誤。
所以更精準地說:
formatPetAge()負責把專案目前可解析的生日資料轉成年齡顯示,並處理空值、無法解析的資料與未來生日。
而不是所有「不合理日期」都一定會被攔下來。
重新看 Git 歷史後,我才發現這幾個函式其實不是同一次建立的。
專案先整理了:
formatPetBirthday()
formatPetGender()
把生日和性別的顯示邏輯集中到:
src/utils/petDisplay.js
後來遇到這次 Dashboard 年齡顯示的 Bug,才再把:
formatPetAge()
加入同一個共用 utility。
而修正後,PetCard 和 PetProfileModal 都可以使用同一套年齡顯示邏輯。
這讓我開始理解,共用函式不一定是一開始就規劃好全部需求。
有時候是專案慢慢開發、問題慢慢出現之後,才發現:
這個規則不應該散落在不同元件裡。
如果每個畫面都自己算一次年齡,之後只要顯示規則改變,就很容易出現不同地方顯示不同結果。
把它集中到共用工具後,元件只需要負責顯示結果。
以前我對 Debug 的想像比較像:
程式執行失敗
→ Console 出現紅色錯誤
→ 根據錯誤訊息修改
但這次不是。
API 有成功回應。
元件也有正常顯示。
寵物名稱和照片都看得到。
只有年齡變成:
-
這種問題不會直接跳出一句:
你的 age 和 birthday 沒有對上
只能自己把流程拆開來看。
畫面現在顯示什麼?
→ 元件正在讀哪個欄位?
→ 實際拿到的資料有哪些欄位?
→ Mock data 和 API Response 有什麼不同?
→ birthday 要經過什麼轉換才能顯示成年齡?
確認完這些地方後,問題才慢慢縮小。
最後真正的原因其實很單純:
Mock data
→ 有 age
真實 API
→ 有 birthday,沒有 age
舊 PetCard
→ 還在讀 pet.age
把修正前後放在一起看,就更清楚。
修正前
PetCard
→ 直接讀取 pet.age
→ API 沒有 age
→ 顯示 -
找到問題後,改成:
修正後
PetCard
→ 使用 pet.birthday
→ formatPetAge(pet.birthday)
→ 顯示實際年齡
這次我才真正感受到,Debug 不一定是在找一段「寫錯的程式碼」。
有時候每一段程式各自看起來都沒有壞掉,真正出問題的是:
資料來源已經改變,但使用資料的地方還停留在原本的寫法。
也因為這樣,我後來在串接 API 時,會更注意前端原本使用的資料結構,和真實 API Response 到底是不是一樣。
所以真正需要修的不是 API。
而是前端要跟著真實資料結構一起調整。
當年齡終於正常顯示時,我其實很有成就感。
不是因為這是一個多複雜的功能。
而是這次沒有一個錯誤訊息直接告訴我:
問題就在這裡。
我是從畫面上的一個 - 開始,慢慢往回確認:
畫面
→ 元件
→ 資料
→ 欄位
→ 日期
→ 年齡顯示
最後才找到真正的原因。
這也讓我開始改變遇到 Bug 時的反應。
以前看到畫面不對,我可能會先懷疑版面,或直接進入元件裡不斷修改。
但後來我會先問:
畫面現在拿到的資料,到底長什麼樣子?
因為畫面只是最後看到的結果。
真正的問題,很可能早就在資料取得、欄位對應或格式轉換的過程中發生了。
如果現在再遇到同樣的問題,我會先做三件事:
確認畫面正在讀哪個欄位
→ 看實際取得的資料
→ 比對 API Response 和前端使用方式
例如這次:
畫面讀 pet.age
接著確認資料:
API 有 birthday
但沒有 age
那問題就已經縮小到:
birthday
→ age display
接下來才去處理日期轉換和年齡計算。
這樣比一開始就在元件裡到處修改,更容易知道自己現在到底在排查哪一層。
這次可以先記住幾件事:
console.log() 可以幫助確認目前真正拿到的資料。這次最重要的學習是:
Debug 的重點不是一直亂試,而是一次確認一個環節。
當我不知道答案時,不需要一開始就猜中 Bug 在哪裡。
可以先從目前確定的事情開始:
畫面顯示不正確
→ 確認畫面讀取的欄位
→ 確認實際取得的資料
→ 比對兩邊的差異
→ 再處理真正有問題的地方
每確認一個環節,就能排除一部分可能性。
Debug 還是會讓人懷疑人生。
但至少這次之後,我不再只是對著程式碼亂改。
而是開始知道:
不知道問題在哪裡的時候,就先想辦法把問題範圍縮小。
下一篇,我想從手機版選取醫院卡片的情境出發。
功能其實已經有反應,但畫面還停在列表裡,使用者看不到地圖上的選取結果。
功能成功,真的就代表使用者感受得到嗎?
Day 16,我會繼續整理這次手機版操作上的問題,以及我開始理解的 UX 差異。
下一篇:
Day 16|功能明明有反應,為什麼手機用起來還是不順?